package bancosys.tec.rtm.exception;

import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.WriteAbortedException;

import org.apache.log4j.Logger;

import bancosys.tec.rtm.exception.util.ReuseObjectOutputStream;

/**
 * @author Koji
 */
public class FileBackupExceptionPersister implements BackupExceptionPersister {

    public static final long FULL_RESTORE_NUMBER = Long.MAX_VALUE;

    public static final String FULL_RESTORE_STRING = "FULL";

    /**
     * Caminho do arquivo de backup
     */
    private String backupFileName = null;

    /**
     * Número de registros que tem que ser gravados do backup para persistencia principal.
     */
    private long numberOfExceptionsToRestore;

    /**
     * Construtor default.
     */
    public FileBackupExceptionPersister() {
        this.numberOfExceptionsToRestore = FULL_RESTORE_NUMBER;
    }

    /**
     * Construtor que recebe o caminho onde será gravado o backup.
     * 
     * @param backupFileName String
     * @throws RTMException e
     */
    public FileBackupExceptionPersister(String backupFileName) throws RTMException {
        this(backupFileName, null);
    }

    /**
     * Construtor que recebe o caminho onde será gravado o backup e o número registros a serem restaurados para persistencia de exceptions
     * principal quando o backup para a mesma for feito. Se for necessario restaurar todas exceptions usar a palavra reservada "FULL".
     * 
     * @param backupFileName String
     * @param numberOfExceptionsToRestore String
     * @throws RTMException e
     */
    public FileBackupExceptionPersister(String backupFileName, String numberOfExceptionsToRestore) throws RTMException {
        this.backupFileName = backupFileName;

        if (numberOfExceptionsToRestore == null || numberOfExceptionsToRestore.equalsIgnoreCase(FULL_RESTORE_STRING)) {
            this.numberOfExceptionsToRestore = FULL_RESTORE_NUMBER;
        } else {
            try {
                this.numberOfExceptionsToRestore = Long.parseLong(numberOfExceptionsToRestore);
            } catch (Throwable e) {
                this.numberOfExceptionsToRestore = FULL_RESTORE_NUMBER;
            }
        }
        try {
            File file = new File(backupFileName);
            if (!file.exists()) {
                file.createNewFile();
                file.delete();
            }
        } catch (IOException e) {
            Logger.getLogger(this.getClass()).error("Erro criando arquivo '" + this.backupFileName + "' para backup do RTM", e);
        }
    }

    /**
     * Grava a Throwable no arquivo de backup.
     * 
     * @param exception {@link Throwable}
     * @throws IOException e
     */
    public synchronized void store(Throwable exception) throws IOException {
        Closeable cl = null;
        try {
            ObjectOutputStream out = this.openFileForWrite();
            cl = out;
            out.writeObject(exception);
            out.flush();
        } catch (Throwable t) {
            Logger.getLogger(this.getClass()).error(
                    "Erro!!! Está exception será somente logada. Não pode ser gravada no backup pois ela contém dados não serializaveis.",
                    exception);
        } finally {
            if (cl != null) {
                cl.close();
            }
        }
    }

    /**
     * Método para carregar e salvar os registros do backup no persister principal passado no parametro.
     * 
     * @param exceptionPersister {@link ExceptionPersister}
     * @throws RTMException e
     */
    public synchronized void loadAndSaveBackupInExceptionPersister(ExceptionPersister exceptionPersister) throws RTMException {
        if (this.backupFileName == null) {
            throw new RTMException("Erro! O caminho do arquivo de backup não está especificado.");
        }

        FileInputStream inputStream = null;
        ObjectInputStream in = null;
        boolean throwableHappens = false;
        try {
            File file = new File(this.backupFileName);
            if (!file.exists()) {
                return;
            }
            inputStream = new FileInputStream(this.backupFileName);
            in = new ObjectInputStream(inputStream);

            boolean eof = false;

            long exceptionsRecorded = 0;
            while (!eof && exceptionsRecorded < this.numberOfExceptionsToRestore) {
                try {
                    Throwable erro = (Throwable) in.readObject();
                    Logger.getLogger(this.getClass()).debug("Restaurando backup da exceção: " + erro.getMessage() + "...");
                    exceptionPersister.persist(erro);
                    if (!(this.numberOfExceptionsToRestore == FULL_RESTORE_NUMBER)) {
                        exceptionsRecorded++;
                    }
                } catch (EOFException e) {
                    eof = true;
                } catch (WriteAbortedException e) {
                    Logger
                            .getLogger(this.getClass())
                            .error("Erro ocorrido no momento de recuperar uma exception. "
                            + "Esta exception não foi gravada corretamente por não ser serializable...",
                            e);
                } catch (Throwable e) {
                    Logger.getLogger(this.getClass()).error("Erro lendo e salvando dados de backup do RTM.", e);
                    throwableHappens = true;
                    this.store(e);
                    break;
                }
            }
        } catch (Exception e) {
            Logger.getLogger(this.getClass()).error(
                    "Erro carregando e salvando dados de backup do RTM do arquivo '" + this.backupFileName + "'.", e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Logger.getLogger(this.getClass()).error("Erro ao fechar o arquivo de backup do RTM: '" + this.backupFileName + "'.", e);
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    Logger.getLogger(this.getClass()).error("Erro ao fechar o arquivo de backup do RTM: '" + this.backupFileName + "'.", e);
                }
            }
            if (throwableHappens) {
                this.renameBackup(this.backupFileName);
            }
        }
    }

    /**
     * Retorna quantos registros foram salvos no backup.
     * 
     * @return listSize
     */
    public synchronized long getSize() {
        long size;
        File file = new File(this.backupFileName);
        if (!file.exists()) {
            size = 0;
        } else {
            size = file.length();
        }
        return size;

    }

    /**
     * Verifica se não existem registros salvos no backup.
     * 
     * @return isEmpty
     */
    public synchronized boolean isEmpty() {
        return this.getSize() == 0;
    }

    /**
     * Limpa os registros do backup.
     * 
     * @throws IOException e
     */
    public synchronized void clearBackup() throws IOException {
        File file = new File(this.backupFileName);
        if (file.exists()) {
            file.delete();
        }
    }

    /**
     * Abre um ObjectOutputStream para escrever as exceptions.
     * 
     * @return ObjectOutputStream
     * @throws IOException e
     */
    private ObjectOutputStream openFileForWrite() throws IOException {
        if (this.backupFileName == null) {
            this.createTempFile();
        }
        long tam = (new File(this.backupFileName)).length();
        boolean isClear = (tam == 0);

        if (!isClear) {
            return new ReuseObjectOutputStream(new FileOutputStream(this.backupFileName, true));
        } else {
            return new ObjectOutputStream(new FileOutputStream(this.backupFileName, true));
        }
    }

    /**
     * Cria um arquivo temporario para realizar o backup das exceptions.
     */
    private void createTempFile() {
        try {
            File file = File.createTempFile("rtm-exception", ".bk");
            this.backupFileName = file.getAbsolutePath();
        } catch (Throwable e) {
            Logger.getLogger(this.getClass()).error("Erro criando arquivo temporário para backup do RTM !!!");
        }
    }

    /**
     * Renomeia o arquivo de backup para nome dele com um sequencial.
     * 
     * @param newBackupFileName String o novo nome do arquivo de backup
     */
    public void renameBackup(String newBackupFileName) {
        long contador = 1;
        String backupFileRenameName = newBackupFileName + "." + contador;
        File backupFileRename = new File(backupFileRenameName);

        while (backupFileRename.exists()) {
            contador++;
            backupFileRenameName = newBackupFileName + "." + contador;
            backupFileRename = new File(backupFileRenameName);
        }
        File backupFile = new File(newBackupFileName);
        if (!backupFile.renameTo(backupFileRename)) {
            Logger.getLogger(this.getClass()).error("Não foi possivel renomear o arquivo de backup...");
        }
    }
}